目前為止,我們已經完成了 LLaMA2 模型的基本架構,但如果要真正開始訓練,第一步就必須先處理文字的輸入,在 NLP 任務中,Tokenizer 負責把「自然語言」轉換成「數字序列 (tokens)」,這些 token 才能進一步送進模型中,今天我們就來看看常見的 Tokenizer 類型,並動手訓練一個屬於我們自己的 BPE Tokenizer。
我們的語言是字母、詞彙與標點符號的組合,電腦並不能直接理解,舉例像是:
輸入: "Hello, world!"
輸出: [1543, 11, 872, 0] # 轉換成 token ids
不同的 Tokenizer 的切割方法,會影響到詞典大小 (vocab size)、模型的泛化能力、訓練效率與推理速度。
這是最簡單的方式,直接以空格和標點來分割,優點就是比較直觀,但比較不適合處理未收錄詞、中文等無空格語言
Input: "Hello, world!"
Output: ["Hello", ",", "world", "!"]
逐字切分,每個字就是一個 token,優點是不會有 OOV 適合所有語言,但缺點就是會讓序列太長,語義單位太小,導致訓練效率極低。
Input: "Hello"
Output: ["H", "e", "l", "l", "o"]
介於「字」和「詞」之間,是目前最常見的 NLP 分詞方式。
Input: "lower" → ["low", "er"]
Input: "newest" → ["new", "est"]
Input: "unhappiness" → ["un", "##happiness"]
Input: "newest" → ["new", "est"]
我們選擇 Hugging Face tokenizers 來訓練。
pip install tokenizers datasets transformers
import json
import os
def read_texts_from_jsonl(file_path: str):
with open(file_path, 'r', encoding='utf-8') as f:
for line in f:
data = json.loads(line)
if 'text' in data:
yield data['text']
def create_tokenizer_config(save_dir: str):
config = {
"bos_token": "<|im_start|>",
"eos_token": "<|im_end|>",
"unk_token": "<unk>",
"pad_token": "<pad>",
"tokenizer_class": "PreTrainedTokenizerFast",
}
with open(os.path.join(save_dir, "tokenizer_config.json"), "w", encoding="utf-8") as f:
json.dump(config, f, ensure_ascii=False, indent=4)
from tokenizers import Tokenizer, models, pre_tokenizers, decoders, trainers
from tokenizers.normalizers import NFKC
def train_tokenizer(data_path: str, save_dir: str, vocab_size: int = 8192):
os.makedirs(save_dir, exist_ok=True)
tokenizer = Tokenizer(models.BPE(unk_token="<unk>"))
tokenizer.normalizer = NFKC()
tokenizer.pre_tokenizer = pre_tokenizers.ByteLevel(add_prefix_space=False)
tokenizer.decoder = decoders.ByteLevel()
special_tokens = ["<unk>", "<s>", "</s>", "<|im_start|>", "<|im_end|>", "<pad>"]
trainer = trainers.BpeTrainer(
vocab_size=vocab_size,
special_tokens=special_tokens,
min_frequency=2,
show_progress=True,
initial_alphabet=pre_tokenizers.ByteLevel.alphabet()
)
texts = list(read_texts_from_jsonl(data_path))
tokenizer.train_from_iterator(texts, trainer=trainer, length=len(texts))
tokenizer.save(os.path.join(save_dir, "tokenizer.json"))
create_tokenizer_config(save_dir)
print(f"Tokenizer saved to {save_dir}")
from transformers import AutoTokenizer
def eval_tokenizer(tokenizer_path: str):
tokenizer = AutoTokenizer.from_pretrained(tokenizer_path)
print("Vocab size:", len(tokenizer))
print("Special tokens:", tokenizer.all_special_tokens)
test_text = "<|im_start|>user\nHello<|im_end|>"
encoded = tokenizer(test_text).input_ids
decoded = tokenizer.decode(encoded, skip_special_tokens=False)
print("Original:", test_text)
print("Decoded:", decoded)
參考連結:
https://datawhalechina.github.io/happy-llm/#/